package org.scalaconsole.ui

import java.awt.{BorderLayout, Font, Toolkit}
import java.awt.event._
import java.awt.event.KeyEvent._

import java.io.{ByteArrayOutputStream, File, OutputStream, PrintStream, PrintWriter, StringWriter}

import javax.swing._

import scala.tools.nsc.{Interpreter, InterpreterResults, Settings}

class ScalaConsole extends Runnable {
  import ScalaConsole._

  /**
   * Path to scala-library.jar
   */
  var libraryPath: Option[String] = None

  /**
   * Path to scala-compiler.jar
   */
  var compilerPath: Option[String] = None

  /* UI */
  private val frame = new JFrame("ScalaConsole")
  private val editor = new JEditorPane
  private val outputPane = new JTextPane
  private val statusLabel = new JLabel
  private var font = Preferences.defaultFont
  private lazy val commandList: String = {
    val result = new StringBuilder("Available commands:").append(newLine)
    Command.list foreach (command => {
      result.append(command.description).append(":\t").
        append(command.getKeyModifiersText).
        append(command.getKeyText).append(newLine)
    })
    result.toString
  }
  private lazy val introText: String = {
    val intro = new StringBuilder("Welcome to ScalaConsole.").append(newLine).append(newLine).
      append("Enter Scala code above and use the 'Execute' command to run it.").
      append(newLine).append(newLine)

    intro.append(commandList)
    intro.toString
  }
  private lazy val resetText = "Console reset." + newLine + newLine + commandList

  /* I/O */
  private var interpreter: Interpreter = null //not intended to be null for very long
  private val writer = new StringWriter
  private val sysout = {
    val output = new ByteArrayOutputStream()
    System.setOut(new PrintStream(output))
    output
  }

  init()

  private def init(): Unit = {
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
    frame.setSize(800, 600)

    /* center on the screen */
    val toolkit = Toolkit.getDefaultToolkit
    val screenSize = toolkit.getScreenSize
    frame.setLocation(
      (screenSize.width - frame.getWidth) / 2,
      (screenSize.height - frame.getHeight) / 2)

    outputPane setEditable false

    editor setFont font
    outputPane setFont font

    val editorScroller = new JScrollPane(editor)
    val outputScroller = new JScrollPane(outputPane)

    val splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, editorScroller, outputScroller)
    splitPane.setOneTouchExpandable(true)
    splitPane.setResizeWeight(0.5)

    statusLabel.setText(Idle.label)

    frame.add(splitPane, BorderLayout.CENTER)
    frame.add(statusLabel, BorderLayout.PAGE_END)
  }

  def run(): Unit = {
    //TODO if paths are not defined, prompt for them to be defined through the UI

    /* paths must be defined at this point */
    reset()

    addAction(editor, Execute, execute())
    addAction(editor, Reset, reset())
    addAction(editor, IncreaseFont, setFontSize(font.getSize + 1))
    addAction(editor, DecreaseFont, setFontSize(font.getSize - 1))

    outputPane setText introText

    frame setVisible true
  }

  private def addAction(component: JComponent, command: Command, action: => Unit) = {
    val actionName = command.toString
    component.getInputMap(command.when).put(
      KeyStroke.getKeyStroke(command.key, command.modifierMask), actionName)
    component.getActionMap.put(actionName, function2Action(action))
  }

  private implicit def function2Action(f: => Unit) = new AbstractAction() {
    def actionPerformed(e: ActionEvent) = f
  }

  private def setFontSize(newSize: Int): Unit = {
    font = new Font(font.getName, font.getStyle, java.lang.Math.max(newSize, 1))

    editor setFont font
    editor invalidate

    outputPane setFont font
    outputPane invalidate
  }

  private def resetOutput(): Unit = {
    clear(writer)
    sysout.reset
  }

  private def reset(): Unit = {
    interpreter = createInterpreter(libraryPath.get, compilerPath.get)
    resetOutput()
    outputPane setText resetText
  }

  private def execute(): Unit = {
    clear(outputPane)
    statusLabel setText Running.label
    statusLabel invalidate

    def flushOutput(): Unit = {
      val sysoutString = sysout.toString
      if (sysoutString.length > 0) {
        appendText(outputPane, newLine)
        appendText(outputPane, sysoutString)
      }
      appendText(outputPane, newLine + writer.toString)
      resetOutput()
    }

    def interpreterError(): Unit = {
      statusLabel setText Error.label
      flushOutput()
    }

    var result: InterpreterResults.Result = InterpreterResults.Success
    var constructedLine: Option[String] = None
    editor.getText.trim.split(newLine) foreach (line => {
      constructedLine = constructedLine match {
        case None => {
          appendText(outputPane, newLine)
          appendText(outputPane, "scala> " + line)
          if (line.trim.length > 0) {
            Some(line)
          }
          else {
            None
          }
        }
        case Some(cl) => {
          if (cl.trim.length > 0) {
            appendText(outputPane, "       " + line)
          }
          Some(cl + line + newLine)
        }
      }

      result = constructedLine match {
        case Some(cl) => {
          val res = interpreter.interpret(cl)
          flushOutput()
          res
        }
        case None => InterpreterResults.Incomplete
      }

      result match {
        case InterpreterResults.Error => interpreterError; return
        case InterpreterResults.Incomplete => //no action necessary
        case InterpreterResults.Success => constructedLine = None
      }

    })

    if (result == InterpreterResults.Incomplete) {
      appendText(outputPane, "Error: statement is incomplete")
      interpreterError
      return
    }

    statusLabel setText Idle.label
  }

  private def clear(pane: JTextPane) = {
    pane setText ""
  }

  private def clear(writer: StringWriter) = {
    val buf = writer.getBuffer
    buf.delete(0, buf.length)
  }

  private def appendText(pane: JEditorPane, text: String) = {
    pane.setText(pane.getText + text)
    pane.invalidate
  }

  private def createInterpreter(libraryPath: String, compilerPath: String): Interpreter = {
    val settings = new Settings(null)
    settings.usejavacp.value = true
    if (!settings.classpath.value.contains(libraryJarName)) {
      settings.classpath.value += File.pathSeparator + libraryPath
    }
    if (!settings.classpath.value.contains(compilerJarName)) {
      settings.classpath.value += File.pathSeparator + compilerPath
    }

    new Interpreter(settings, new PrintWriter(writer))
  }
}

object ScalaConsole {
  val newLine = "\n"
  val libraryJarName = "scala-library.jar"
  val compilerJarName = "scala-compiler.jar"

  abstract sealed class Status {
    def label: String
  }
  case object Idle extends Status { def label = "Ready" }
  case object Running extends Status { def label = "Running" }
  case object Error extends Status { def label = "Error" }
}
